第6章 集合引用类型(1)

Object类型

适合存储数据,使用 new 操作符和 Object 构造函数创建实例:

let person = new Object();
person.name = "Nicholas";
person.age = 29;

更常用的方式是使用对象字面量的方式,对象中包含的属性一目了然:

let person = {
  name: "Nicholas",
  age: 29
};

通过点操作符或者中括号来存取属性:

console.log(person["name"]); // "Nicholas"
console.log(person.name);    // "Nicholas"

两种存取属性的方式没有区别,但是中括号往往用于处理一些特殊情况:

// 通过变量访问属性
let propertyName = "name";
console.log(person[propertyName]); // "Nicholas"
// 当属性名中包含特殊字符、关键字、保留字时
person["first name"] = "Nicholas";

Array类型

存储有序的数据,数组中可以存放不同类型的数据,也是通过 new 操作符创建实例:

let colors = new Array();
// 可以给构造函数传入一个数值,用来定义数组长度
let colors = new Array(20);
// 也可以给 Array 构造函数传入要保存的元素
let colors = new Array("red", "blue", "green");

同样也可以通过字面量方式进行创建:

let colors = ["red","blue","green"]; //创建一个包含3个元素的数组 
let names = []; // 创建一个空数组
let values = [1,2,]; // 创建一个包含 2 个元素的数组

ES6还提供了两个创建数组的静态方法:from() 和 of(),from() 用于将类数组结构转换为数组实例,而 of() 用于将一组参数转换为数组实例

// Array.from()的第一个参数是一个类数组对象,即任何可迭代的结构
console.log(Array.from("Matt")); // ["M", "a", "t", "t"]

// Array.from()对现有数组执行浅拷贝
const a1 = [1, 2, 3, 4];
const a2 = Array.from(a1);
console.log(a1); // [1, 2, 3, 4]
console.log(a1 === a2); // false

// arguments 对象也可以被转换为数组 
function getArgsArray() {
  return Array.from(arguments);
}
console.log(getArgsArray(1, 2, 3, 4)); // [1, 2, 3, 4]

// Array.from()还接收第二个可选的映射函数参数,用来处理新数组的值
const a1 = [1, 2, 3, 4];
const a2 = Array.from(a1, x => x**2);
console.log(a2); // [1, 4, 9, 16]

// 还可以接收第三个可选参数,用于指定映射函数中 this 的值
// 这个重写的 this 值在箭头函数中不适用
const a3 = Array.from(a1, function(x) {
  return x**this.exponent
}, {
  exponent: 2
});
console.log(a3); // [1, 4, 9, 16]

// Array.of()可以把一组参数转换为数组
console.log(Array.of(1, 2, 3, 4)); // [1, 2, 3, 4]
console.log(Array.of(undefined));  // [undefined]

使用数组字面量初始化数组时,可以使用一串逗号来创建空位:

const options = [,,,,,]; // 创建包含 5 个元素的数组 
console.log(options.length); // 5
console.log(options); // [,,,,,]
// ES6普遍将这些空位当成存在的元素,只不过值为 undefined
const options = [1,,,,5];
for (const option of options) {
  console.log(option === undefined);
}
// false
// true
// true
// true
// false

// 但是 ES6 之前的方法会忽略空位,但具体行为因方法而异
const options = [1,,,,5];
// map()会跳过空位置
console.log(options.map(() => 6)); // [6, undefined, undefined, undefined, 6]
// join()视空位置为空字符串 
console.log(options.join('-')); // "1----5"

// 由于行为不一致和存在性能隐患,因此实践中要避免使用数组空位
// 实在非要用空位,则显式地用 undefined 值代替
const options = [1, undefined, undefined, undefined, 5];
console.log(options.map(() => 6)); // [6, 6, 6, 6, 6]

数组通过索引来存取数据:

let colors = ["red", "blue", "green"]; // 定义一个字符串数组
alert(colors[0]); // 显示第一项
colors[2] = "black"; // 修改第三项
colors[3] = "brown"; // 添加第四项

数组通过 length 属性获取数组长度,也可修改该属性达到从数组末尾删除或添加元素的效果:

let colors = ["red", "blue", "green"]; // 创建一个包含3个字符串的数组
colors.length = 2; // 修改数组长度为2
alert(colors[2]); // undefined,删除了最后一个(位置2的)值
// 可以通过length向数组末尾追加元素
colors[colors.length] = "black"; // 添加一种颜色(位置2) 
colors[colors.length] = "brown"; // 再添加一种颜色(位置3)
console.log(colors); // ["red", "blue", "black", "brown"]

判断一个对象是不是数组:

// 只有一个网页的情况下
if (value instanceof Array){
  // 操作数组
}

// 多个网页的情况下
if (Array.isArray(value)){
  // 操作数组
}

ES6提供了三个数组内容的迭代方法:keys()、values()、entries()

const a = ["foo", "bar", "baz", "qux"];
const aKeys = Array.from(a.keys());
const aValues = Array.from(a.values());
const aEntries = Array.from(a.entries());

console.log(aKeys); // [0, 1, 2, 3]
console.log(aValues); // ["foo", "bar", "baz", "qux"] 
console.log(aEntries); // [[0, "foo"], [1, "bar"], [2, "baz"], [3, "qux"]]

ES6通过 copyWithin() 和 fill() 方法对数组进行填充:

const zeroes = [0, 0, 0, 0, 0];

zeroes.fill(5); // 用 5 填充整个数组
console.log(zeroes); // [5, 5, 5, 5, 5]
zeroes.fill(0); // 重置

// 第二个可选参数,表示内容填充的起始索引
zeroes.fill(6, 3); // 用 6 填充索引大于等于 3 的元素 
console.log(zeroes); // [0, 0, 0, 6, 6]
zeroes.fill(0); // 重置

// 第三个可选参数,表示内容填充的结束位置,不包含该位置
zeroes.fill(7, 1, 3); // 用7填充索引大于等于1且小于3的元素
console.log(zeroes); // [0, 7, 7, 0, 0]; 
zeroes.fill(0); // 重置

// 负值索引表示从数组末尾开始计算
// (-4 + zeroes.length = 1)
// (-1 + zeroes.length = 4) 
zeroes.fill(8, -4, -1); // 用8填充索引大于等于1且小于4的元素
console.log(zeroes); // [0, 8, 8, 8, 0];
zeroes.fill(0); // 重置

// fill()方法会忽略超出范围、索引范围不合理的情况
zeroes.fill(1, -10, -6); // 索引过低,忽略
console.log(zeroes); // [0, 0, 0, 0, 0]
zeroes.fill(1, 10, 15); // 索引过高,忽略
console.log(zeroes); // [0, 0, 0, 0, 0]
zeroes.fill(2, 4, 2); // 索引反向,忽略
console.log(zeroes); // [0, 0, 0, 0, 0]
zeroes.fill(4, 3, 10); // 索引部分可用,填充可用部分
console.log(zeroes); // [0, 0, 0, 4, 4]

copyWithin() 会按照指定范围浅拷贝数组中的部分内容,然后将它们插入到指定索引开始的位置

let ints, reset = () => ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
reset();

// 从 ints 中复制索引 0 开始的内容,插入到索引 5 开始的位置
// 在源索引或目标索引到达数组边界时停止
ints.copyWithin(5);
console.log(ints); // [0, 1, 2, 3, 4, 0, 1, 2, 3, 4] 
reset();

// 从 ints 中复制索引 5 开始的内容,插入到索引 0 开始的位置
ints.copyWithin(0, 5);
console.log(ints); // [5, 6, 7, 8, 9, 5, 6, 7, 8, 9]
reset();

// 从 ints 中复制索引 0 开始到索引 3 结束的内容
// 插入到索引 4 开始的位置
ints.copyWithin(4, 0, 3);
console.log(ints); // [0, 1, 2, 3, 0, 1, 2, 7, 8, 9] 
reset();

// 支持负索引值,与fill()相对于数组末尾计算正向索引的过程是一样的 
ints.copyWithin(-4, -7, -3);
alert(ints); // [0, 1, 2, 3, 4, 5, 3, 4, 5, 6]
reset();

// copyWithin()方法会忽略超出范围、索引范围不合理的情况
ints.copyWithin(1, -15, -12); // 索引过低,忽略
console.log(ints); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 
reset();
ints.copyWithin(1, 12, 15); // 索引过高,忽略
console.log(ints); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 
reset();
ints.copyWithin(2, 4, 2); // 索引反向,忽略
console.log(ints); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
reset();
ints.copyWithin(4, 7, 10) // 索引部分可用,复制、填充可用部分
console.log(ints); // [0, 1, 2, 3, 7, 8, 9, 7, 8, 9];

数组通过 join() 方法,将数组内容连接起来,通过参数调整连接数组元素的字符

let colors = ["red", "green", "blue"];
alert(colors.join(","));     // red,green,blue
alert(colors.join("||"));    // red||green||blue

数组对象可以像栈一样进行 push 插入和 pop 删除操作

let colors = new Array(); // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
alert(count); // 2
count = colors.push("black"); // 再推入一项
alert(count); // 3
let item = colors.pop(); // 取得最后一项
alert(item); // black
alert(colors.length); // 2

数组对象也可以像队列一样进行 push 插入和 shift 删除操作

let colors = new Array(); // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
alert(count); // 2
count = colors.push("black"); // 再推入一项
alert(count); // 3
let item = colors.shift(); // 取得第一项
alert(item); // red
alert(colors.length); // 2

也可以从队列的头部开始插入和删除,分别用到 unshift() 和 pop() 方法

let colors = new Array(); // 创建一个数组
let count = colors.unshift("red", "green"); // 从数组开头推入两项 
alert(count); // 2
count = colors.unshift("black"); // 再推入一项
alert(count); // 3
let item = colors.pop(); // 取得最后一项
alert(item); // green
alert(colors.length); // 2

数组有两个方法可以用来对元素重新排序:reverse() 和 sort()

let values = [1, 2, 3, 4, 5];
values.reverse();
alert(values);  // 5,4,3,2,1

默认情况下 sort() 会按照升序进行排序,即最小的值在前面,最大的值在后面。但是比较方式是将数组元素通过 String() 方法转型,再比较字符串大小

let values = [0, 1, 5, 10, 15];
values.sort();
alert(values);  // 0,1,10,15,5

显然这样做是有局限性的,因此 sort() 方法可以接收一个比较函数,比较函数接收两个参数,如果第一个参数应该排在第二个参数前面,就返回负值;如果两个参数相等,就返回 0;如果第一个参数应该排在第二个参数后面,就返回正值。

function compare(value1, value2) {
  if (value1 < value2) {
    return -1;
  } else if (value1 > value2) {
    return 1;
  } else {
    return 0; 
  }
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); // 0,1,5,10,15

如果需要降序排序,则只需要把 compare() 方法的返回值交换一下即可,也可以将函数简化成箭头函数:

let values = [0, 1, 5, 10, 15];
values.sort((a, b) => a < b ? 1 : a > b ? -1 : 0);
alert(values); // 15,10,5,1,0

数组通过 concat() 方法进行连接

let colors = ["red", "green", "blue"];
let colors2 = colors.concat("yellow", ["black", "brown"]);
console.log(colors); // ["red", "green","blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]

以上例子中,concat()方法默认会将传入的参数打散,再将元素添加至目标数组,最终形成一个元素独立的数组。如果不想让数组参数打散,可以通过特殊符号:Symbol.isConcatSpreadable 进行控制:

let colors = ["red", "green", "blue"];
let newColors = ["black", "brown"];
newColors[Symbol.isConcatSpreadable] = false;
let moreNewColors = {
  [Symbol.isConcatSpreadable]: true,
  length: 2,
  0: "pink",
  1: "cyan"
};
// 强制不打散数组
let colors2 = colors.concat("yellow", newColors);
// 强制打散数组
let colors3 = colors.concat(moreNewColors);
console.log(colors); // ["red", "green", "blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", ["black", "brown"]]
console.log(colors3); // ["red", "green", "blue", "pink", "cyan"]

通过 slice() 方法对数组进行切片,可以接收一个或两个参数,用来规定开始索引和结束索引,如果参数是负数,则表示从数组末尾开始计算:

let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
alert(colors2);  // green,blue,yellow,purple
let colors3 = colors.slice(1, 4);
alert(colors3);  // green,blue,yellow
let colors4 = colors.slice(1, -2);
alert(colors4);  // green,blue

使用 splice() 方法向数组中间插入元素,共有3种不同的方式使用:

splice() 方法始终返回这样一个数组,它包含从数组中被删除的元素(如果没有删除元素,则返回空数组)

let colors = ["red", "green", "blue"];
let removed = colors.splice(0,1); // 删除第一项
alert(colors); // green,blue 
alert(removed); // red,只有一个元素的数组
removed = colors.splice(1, 0, "yellow", "orange"); // 在位置 1 插入两个元素
alert(colors); // green,yellow,orange,blue
alert(removed); // 空数组
removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
alert(colors); // green,red,purple,orange,blue 
alert(removed); // yellow,只有一个元素的数组

数组有两类搜索方法:严格相等搜索和断言函数搜索

严格相等:indexOf()、lastIndexOf() 和 includes(),这些方法都接收两个参数:要查找的元素和一个可选的起始搜索位置。lastIndexOf() 是从数组末尾向前搜索,其余方法从数组前头向后搜索

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
alert(numbers.indexOf(4)); // 3
alert(numbers.lastIndexOf(4)); // 5
alert(numbers.includes(4)); // true
alert(numbers.indexOf(4, 4)); // 5
alert(numbers.lastIndexOf(4, 4)); // 3
alert(numbers.includes(4, 7)); // false

let person = { name: "Nicholas" }; 
let people = [{ name: "Nicholas" }];
let morePeople = [person];
alert(people.indexOf(person)); // -1
alert(morePeople.indexOf(person));  // 0
alert(people.includes(person)); // false
alert(morePeople.includes(person)); // true

断言函数:可以定义一个函数来搜索数组,断言函数的返回值决定了相应索引的元素是否被认为匹配。断言函数接收3个参数:元素、索引和数组本身。find() 和 findIndex() 方法使用了断言函数,这两个方法都从数组的最小索引开始。find() 返回第一个匹配的元素,findIndex() 返回第一个匹配元素的索引。这两个方法也都接收第二个可选的参数,用于指定断言函数内部 this 的值。一旦找到匹配项后,两个方法就不再继续搜索了。

const people = [
  {
    name: "Matt",
    age: 27
  },
  {
    name: "Nicholas",
    age: 29
  }
];
alert(people.find((element, index, array) => element.age < 28)); // {name: "Matt", age: 27}
alert(people.findIndex((element, index, array) => element.age < 28)); // 0

数组定义了5个迭代方法,每个方法接收两个参数:以每一项为参数运行的函数,以及可选的作为函数运行上下文的作用域对象(影响函数中 this 的值)。传给每个方法的函数接收3个参数:数组元素、元素索引和数组本身。

以上方法中,every() 和 some() 是最相似的,对 every() 来说,传入的函数必须对每一项都返回 true,它才会返回 true,否则,它就返回 false;而对 some() 来说,只要有一项让传入的函数返回 true,它就会返回 true

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let everyResult = numbers.every((item, index, array) => item > 2);
alert(everyResult);  // false
let someResult = numbers.some((item, index, array) => item > 2);
alert(someResult);   // true

filter() 方法用作数组元素过滤器,将满足要求的数据筛选出来

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter((item, index, array) => item > 2);
alert(filterResult);  // 3,4,5,4,3

map() 方法用于对数组元素的再加工,将加工后的结果返回回来

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let mapResult = numbers.map((item, index, array) => item * 2);
alert(mapResult);  // 2,4,6,8,10,8,6,4,2

forEach() 方法用于遍历数组元素,相当于执行了for循环操作

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => { 
  // 执行某些操作
});

数组还提供了两个归并方法:reduce() 和 reduceRight()。这两个方法都会迭代数组的所有项,并在此基础上构建一个最终返回值。reduce() 方法从数组第一项开始遍历到最后一项,而 reduceRight() 从最后一项开始遍历至第一项。这两个方法都接收两个参数:对每一项都会运行的归并函数,以及可选的以之为归并起点的初始值。传给 reduce() 和 reduceRight() 的函数接收 4 个参数:上一个归并值、当前项、当前项的索引和数组本身。这个函数返回的任何值都会作为下一次调用同一个函数的第一个参数。

let values = [1, 2, 3, 4, 5];
let sum = values.reduce((prev, cur, index, array) => prev + cur);
alert(sum);  // 15